import { constants } from 'node:fs'
import { access, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { ensureDir } from './helpers'

export const DEFAULT_VSCODE_ENTRY_OUTPUT = '.vscode/weapp-tailwindcss.intellisense.css'

export const DEFAULT_VSCODE_SOURCES = [
  'not "./dist"',
  'not "./unpackage"',
  './src/**/*.{wxml,axml,swan,qml,ttml,ux,uts}',
  './src/**/*.{js,jsx,ts,tsx}',
  './src/**/*.{vue,svelte,html,md,mdx}',
]

const SINGLE_QUOTE = '\''
const DOUBLE_QUOTE = '"'

export interface GenerateVscodeEntryOptions {
  baseDir: string
  cssEntry: string
  output?: string
  sources?: string[]
  force?: boolean
}

export interface GenerateVscodeEntryResult {
  outputPath: string
  cssEntryPath: string
}

function toPosixPath(filepath: string) {
  return filepath.replace(/\\/g, '/')
}

async function assertFileExists(filepath: string) {
  try {
    await access(filepath, constants.F_OK)
  }
  catch (error) {
    const err = error as NodeJS.ErrnoException
    if (err?.code === 'ENOENT') {
      throw new Error(`CSS entry file not found: ${filepath}`)
    }
    throw err
  }
}

async function assertCanWrite(filepath: string, force?: boolean) {
  try {
    await access(filepath, constants.F_OK)
    if (!force) {
      throw new Error(
        `VS Code helper already exists at ${filepath}. Re-run with --force to overwrite it.`,
      )
    }
  }
  catch (error) {
    const err = error as NodeJS.ErrnoException
    if (err?.code === 'ENOENT') {
      return
    }
    throw err
  }
}

function toCssLiteral(value: string) {
  const normalized = toPosixPath(value)
  return JSON.stringify(normalized)
}

function formatSource(pattern: string) {
  const trimmed = pattern.trim()
  if (!trimmed) {
    return null
  }

  if (trimmed.startsWith('@source ')) {
    return trimmed.endsWith(';') ? trimmed : `${trimmed};`
  }

  let body = trimmed
  let keyword = ''
  if (body.startsWith('not ')) {
    keyword = 'not '
    body = body.slice(4).trim()
  }
  else if (body.startsWith('!')) {
    keyword = 'not '
    body = body.slice(1).trim()
  }

  if (!body) {
    throw new Error('Invalid @source pattern: empty body.')
  }

  if (!body.startsWith(SINGLE_QUOTE) && !body.startsWith(DOUBLE_QUOTE)) {
    body = toCssLiteral(body)
  }

  return `@source ${keyword}${body};`
}

function resolveOutputPath(baseDir: string, output?: string) {
  const target = output ?? DEFAULT_VSCODE_ENTRY_OUTPUT
  return path.isAbsolute(target) ? path.normalize(target) : path.resolve(baseDir, target)
}

function resolveCssEntry(baseDir: string, entry: string) {
  return path.isAbsolute(entry) ? path.normalize(entry) : path.resolve(baseDir, entry)
}

function toRelativeImport(fromFile: string, targetFile: string) {
  const fromDir = path.dirname(fromFile)
  let relative = path.relative(fromDir, targetFile)
  if (!relative) {
    relative = path.basename(targetFile)
  }
  if (!relative.startsWith('.')) {
    relative = `./${relative}`
  }
  return toPosixPath(relative)
}

export async function generateVscodeIntellisenseEntry(
  options: GenerateVscodeEntryOptions,
): Promise<GenerateVscodeEntryResult> {
  const baseDir = options.baseDir
  const cssEntryPath = resolveCssEntry(baseDir, options.cssEntry)
  await assertFileExists(cssEntryPath)

  const outputPath = resolveOutputPath(baseDir, options.output)
  await ensureDir(path.dirname(outputPath))
  await assertCanWrite(outputPath, options.force)

  const sources = options.sources && options.sources.length > 0
    ? options.sources
    : DEFAULT_VSCODE_SOURCES
  const formattedSources = sources
    .map(formatSource)
    .filter((statement): statement is string => Boolean(statement))

  const cssImport = toRelativeImport(outputPath, cssEntryPath)

  const separator = formattedSources.length > 0 ? [''] : []

  const content = [
    '/*',
    ' * Auto-generated by weapp-tailwindcss.',
    ' * This file exists solely to activate Tailwind CSS IntelliSense in VS Code.',
    ' * Do not import it in your actual mini-program bundles.',
    ' */',
    '@import \'tailwindcss\';',
    '',
    ...formattedSources,
    ...separator,
    `@import '${cssImport}';`,
    '',
  ]
    .filter((line, idx, arr) => !(line === '' && arr[idx - 1] === ''))
    .join('\n')

  await writeFile(outputPath, `${content}\n`, 'utf8')

  return { outputPath, cssEntryPath }
}
